/*
 * Copyright 2008 Nathan C Jones
 * 
 * This file is part of Fivehundred.
 * 
 * Fivehundred is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * Fivehundred is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * Fivehundred. If not, see <http://www.gnu.org/licenses/>. 
 */

package com.googlecode.fivehundred.ui;

import java.awt.Graphics2D;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * A graphical component which is composed of many graphical components.
 * 
 * @author Nathan C Jones
 * @param <E> The type of graphical component contained in the composite.
 */
public class CompositeGraphicalComponent extends AbstractGraphicalComponent implements GraphicalComponent,
		Iterable<GraphicalComponent> {

	/** The components, stored in their Z order. */
	private List<GraphicalComponent> components = new ArrayList<GraphicalComponent>();

	/**
	 * Add a graphical component so that it has a z position greater than any
	 * other component added so far. The added component will have its parent set
	 * to this component. The added component must not already have a parent.
	 * 
	 * @param component the component to add.
	 * @throws IllegalStateException if the component being added already has a
	 *         parent component.
	 */
	public void add(GraphicalComponent component) {
		if (component.getParent() != null) {
			throw new IllegalStateException("Cannot add a component when it already has a parent.");
		}
		component.setParent(this);
		components.add(component);
	}

	/**
	 * Remove a graphical component from the composite component. Has no effect
	 * if the component was not a child of the composite.
	 * 
	 * @param component the component to remove from the composite.
	 */
	public void remove(GraphicalComponent component) {
		components.remove(component);
	}

	/**
	 * <p>
	 * Set the Z position for the component. The given Z position can be any
	 * value, but if it is less than zero or greater than the number of
	 * components in the composite then it will be adjusted so it is within these
	 * bounds. For example, if there are three components in the composite and
	 * one of them has its Z position changed to 500, then it's actual new Z
	 * position will be 2.
	 * </p>
	 * <p>
	 * The Z position of a component is unique but it is not preserved. If a
	 * multiple component are set to have the same Z position, the component that
	 * was set latest will have the given Z value, and previously set components
	 * will be pushed forward and given higher Z positions.
	 * </p>
	 * <p>
	 * If the given component is not a child of the composite no action is taken.
	 * </p>
	 * 
	 * @param component the component to change the z position for.
	 * @param zPosition the new z position for the component.
	 */
	public void setZPosition(GraphicalComponent component, int zPosition) {
		// TODO locking - cannot allow components to be added or removed while
		// changing a component's z order - may also cause problems if there is
		// already an iterator accessing the items.
		final boolean isExistingChild = components.remove(component);
		if (isExistingChild) {
			final int index = zPosition < 0 ? 0 : zPosition > components.size() ? components.size() : zPosition;
			components.add(index, component);
		}
	}

	/**
	 * Get the Z position for a child component of the composite. The Z position
	 * for a component will be between zero and one less than the number of
	 * components in the composite (inclusive).
	 * 
	 * @param component
	 * @return the Z position of the given component, or <code>-1</code> if the
	 *         given component's parent
	 */
	public int getZPosition(GraphicalComponent component) {
		return components.indexOf(component);
	}

	/**
	 * Get an iterator for iterating over the child components. The iterator will
	 * return components according to their Z order. The iterator cannot be used
	 * to modify the state of the composite component.
	 * 
	 * @return an iterator of the composite's graphical components.
	 */
	@Override
	public Iterator<GraphicalComponent> iterator() {
		return Collections.unmodifiableCollection(components).iterator();
	}

	/**
	 * Check if this component, or any of its descendant components contain a
	 * given point.
	 * 
	 * @param p the point to check if the component contains.
	 * @param x the absolute horizontal position of this component's parent,
	 *        relative to the root container.
	 * @param y the absolute vertical position of this component's parent,
	 *        relative to the root container.
	 * @return <code>true</code> if any of this composite's children contain
	 *         the given point.
	 */
	@Override
	public boolean contains(Point p, int x, int y, double angle) {
		final Point position = getPosition();
		for (GraphicalComponent c : this) {
			if (c.contains(p, position.x + x, position.y + y, getAngle() + angle)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Causes all of the contained graphical components to be painted. They will
	 * be painted according to their Z order.
	 * 
	 * @param g the graphics object to paint the component onto.
	 * @param x the absolute horizontal position of this component's parent,
	 *        relative to the root container.
	 * @param y the absolute vertical position of this component's parent,
	 *        relative to the root container.
	 * @param angle the absolute angle of this container's parent.
	 */
	@Override
	public void paint(Graphics2D g, int x, int y, double angle) {
		final Point position = getPosition();
		for (GraphicalComponent c : this) {
			c.paint(g, position.x + x, position.y + y, getAngle() + angle);
		}
	}

	/**
	 * Get the component within the composite at the given point.
	 * 
	 * @param point the point to find a component at.
	 * @param x the horizontal offset of the parent component relative to the
	 *        origin of the point's coordinate system.
	 * @param y the vertical offset of the parent component relative to the
	 *        origin of the point's coordinate system.
	 * @param angle the angle of the parent component.
	 * @return the descendant component at the given point, or <code>null</code>
	 *         if there is no component at the point.
	 */
	GraphicalComponent getComponentAt(Point point, int x, int y, double angle) {
		GraphicalComponent component = null;
		final Point position = getPosition();
		final int newX = x + position.x;
		final int newY = y + position.y;
		final double newAngle = angle + getAngle();
		for (Iterator<GraphicalComponent> i = new ReverseIterator<GraphicalComponent>(components); i.hasNext();) {
			GraphicalComponent c = i.next();
			if (c instanceof CompositeGraphicalComponent) {
				final CompositeGraphicalComponent composite = (CompositeGraphicalComponent) c;
				component = composite.getComponentAt(point, newX, newY, newAngle);
			} else if (c.contains(point, newX, newY, newAngle)) {
				component = c;
			}
			if (component != null) {
				break;
			}
		}
		return component;
	}

	void repaint() {
		getParent().repaint();
	}

	/**
	 * Create an iterator which traverses a list in reverse direction.
	 * 
	 * @author Nathan C Jones
	 */
	static class ReverseIterator<E> implements Iterator<E> {
		private List<E> list;

		private int index;

		public ReverseIterator(List<E> list) {
			this.list = list;
			this.index = list.size();
		}

		public boolean hasNext() {
			return index > 0;
		}

		public E next() {
			if (index == 0) {
				throw new NoSuchElementException();
			}
			return list.get(--index);
		}

		public void remove() {
			throw new UnsupportedOperationException();
		}
	}
}
